Mobile Development: PingNG-the next generation ping
As I needed a tool to test the maximum MTU size for a network, I needed a ping tool where I can define the packet size and the DF (Do Not Fragment-Flag). As I did not find such a tool, I wrote PingNG.
It is a simple tool but the code had to avoid some pitfalls.
First, if you enter a host name, it has to be translated to an IPAddress before you can use an IcmpEchoReply. But GetHostEntry API call blocks. It will especially block very, very long if you enter an unknown host. If you want to offer a responsive GUI, we need a way to limit the time allowed to resolve the host name:
bool checkHost(ref HostParms hostParms) { bool bRet = false; IPHostEntry ipHost = new IPHostEntry(); try { IPHostEntry entry = null; int maxSeconds = hostParms.retry/* 10 */, counter = 0; // see http://jianmingli.com/wp/?p=22 // Start the asynchronous request for DNS information. // This example does not use a delegate or user-supplied object // so the last two arguments are null. IAsyncResult result = Dns.BeginGetHostEntry(hostParms.host/* ipAddress */, null, null); // Poll for completion information. while (result.IsCompleted != true && counter < maxSeconds) { Thread.Sleep(1000); counter++; } if (result.IsCompleted) //when we got here, the result is ready entry = Dns.EndGetHostEntry(result); //blocks? else hostParms.isValid = false; // Thread.CurrentThread.Abort(); hostParms.ipAddress = entry.AddressList[0]; hostParms.isValid = true; } catch (ThreadAbortException e) { Thread.CurrentThread.Abort();// ("DNS failed within timeout", e); } catch (Exception e) { System.Diagnostics.Debug.WriteLine(string.Format( "Exception in checkHost({0}): {1}", hostParms.host, e.Message)); } return hostParms.isValid; }
The above checkHost function will be called by a thread and will either deliver a valid response or returns after a specified timeout.
As the IcmpReply API call itself blocks too, the ping is wrapped into a thread. The status is delivered to the GUI using delegates and event subscribers.
//thread to do ping public void startPing(string sHost, PingOptions pOptions) { System.Diagnostics.Debug.WriteLine("+++thread started"); string ipAddress = sHost;//make a local copy Ping myPing = new Ping(); PingOptions myPingOptions = pOptions;//make a local copy int iTimeout = pOptions.TimeOut; int _numberOfPings = pOptions.numOfPings; bool doNotFragment = pOptions.DontFragment; int bufferSize = pOptions.bufferSize; byte[] buf = new byte[bufferSize]; //long sumRoundtripTime=0; onReply(new PingReplyEventArgs("ping started...",PingReplyTypes.info)); replyStats _replyStats = new replyStats(ipAddress); PingReply reply = null; try { onReply(new PingReplyEventArgs(pOptions.ToString(), PingReplyTypes.info)); //check DNS first as this may block for long time IPAddress address; HostParms hostParms = new HostParms(ipAddress, 4); try { //is IP address address = IPAddress.Parse(ipAddress); hostParms.isValid = true; } catch { if (checkHost(ref hostParms)) ipAddress = hostParms.ipAddress.ToString(); } if (!hostParms.isValid) throw new PingUnknownHostException("Unkown host: " + ipAddress); for (int ix = 0; ix < _numberOfPings; ix++) { reply = myPing.Send(ipAddress, buf, iTimeout, myPingOptions); string sReply = ""; if (reply.Status == IPStatus.Success) { //sumRoundtripTime += reply.RoundTripTime; _replyStats.add(1, 1, reply.RoundTripTime); sReply = myResources.getReply(reply, ipAddress); } else if (reply.Status == IPStatus.DestinationHostUnreachable) { _replyStats.add(1, 0, reply.RoundTripTime); throw new PingUnknownHostException("Destination unreachable"); } else { _replyStats.add(1, 0, reply.RoundTripTime); sReply = myResources.getReply(reply, ipAddress); } System.Diagnostics.Debug.WriteLine(sReply); onReply(new PingReplyEventArgs(sReply)); } onReply(new PingReplyEventArgs(_replyStats.ToString(), PingReplyTypes.info)); } ...
The small tool could be extended to do automatic sweeps over varying IP addresses or with varying packet sizes.
If you specify a packet size that is beyond the MTU and set the DF flag you will get the correct answer:
ping started... ttl=128, DF=True, buf=1480, #p=4, timeout=1000 IPStatus.PacketTooBig IPStatus.PacketTooBig IPStatus.PacketTooBig IPStatus.PacketTooBig Ping statistics for 192.168.128.5: Packets: Sent = 4, Received = 0, Lost = 4 (100% loss), Approximate round trip times in milli-seconds: Minimum = 0ms, Maximum = 0ms, Average = 0ms done
By reducing the packet size you will get a working ping:
ping started... ttl=128, DF=True, buf=1472, #p=4, timeout=1000 Reply from 192.168.128.5: bytes=1472 time=8ms TTL=128 Reply from 192.168.128.5: bytes=1472 time=5ms TTL=128 Reply from 192.168.128.5: bytes=1472 time=4ms TTL=128 Reply from 192.168.128.5: bytes=1472 time=6ms TTL=128 Ping statistics for 192.168.128.5: Packets: Sent = 4, Received = 4, Lost = 0 (0% loss), Approximate round trip times in milli-seconds: Minimum = 4ms, Maximum = 8ms, Average = 5ms done
The ICMP reply has a lot of possible result codes. PingNG only evaluates some of them in depth. To display the statistics, I created a new class
public class replyStats { public int sent=0; public int rcvd=0; public int percentLost { get { if (sent != 0) return ((lost * 100) / sent); else return 100; } } public long minTrip=0; public long maxTrip=0; public long avgTrip{ get { if (sent == 0) return 0; if (tripValues.Count == 0) return 0; int sum = 0; foreach (int v in tripValues) sum += v; int avg = sum / tripValues.Count; return avg; } } List<long> tripValues=new List<long>(); string ipAddress = ""; public int lost { get { return sent - rcvd; } } public replyStats(string sIPaddress){ ipAddress = sIPaddress; minTrip = long.MaxValue; maxTrip = 0; } public void add(int iSent, int iRcvd, long lTrip) { if (lTrip < minTrip) minTrip = lTrip; if (lTrip > maxTrip) maxTrip = lTrip; tripValues.Add(lTrip); sent += iSent; rcvd += iRcvd; } public override string ToString() { string s = string.Format(ReplyStats, ipAddress, sent, rcvd, lost, percentLost, minTrip, maxTrip, avgTrip); return s; } const string ReplyStats = "Ping statistics for {0}:\r\n" + " Packets: Sent = {1}, Received = {2}, Lost = {3} ({4}% loss),\r\n" + "Approximate round trip times in milli-seconds:\r\n" + " Minimum = {5}ms, Maximum = {6}ms, Average = {7}ms\r\n"; }
This class can report the stats if you add the single ICMP results on every return. For example, for a successful ping:
_replyStats.add(1, 1, reply.RoundTripTime);
There are still many enhancements possible. The project is done in VS2008 using Compact Framework 2.0 and targets Windows CE and will run nice on Windows Mobile too.
The code is hosted at code.google.com (discontinued, now at github) as part of my win-mobile-code repo.